﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using JetBrains.Application.Progress;
using JetBrains.Metadata.Reader.API;
using JetBrains.ReSharper.Feature.Services.CSharp.Bulbs;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Caches;
using JetBrains.ReSharper.Psi.Impl.Types;
using JetBrains.ReSharper.Psi.Modules;
using JetBrains.ReSharper.Psi.Search;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.Util;
using JetBrains.Util.DataStructures;
using MugenMvvmToolkit.ReSharper.PluginModels;
// ReSharper disable once CheckNamespace

namespace MugenMvvmToolkit.ReSharper
{
    public class TypeProvider
    {
        #region Nested types

        private delegate void ConverterDelegate<in T, TResult>(T element, ICollection<TResult> collector) where T : class, IDeclaredElement;

        [PsiComponent]
        internal class TypeProviderCache : InvalidatingPsiCache
        {
            #region Fields

            private readonly CompactMap<Pair<IPsiModule, IModuleReferenceResolveContext>, TypeProvider> _types =
                new CompactMap<Pair<IPsiModule, IModuleReferenceResolveContext>, TypeProvider>(
                    Pair<IPsiModule, IModuleReferenceResolveContext>.EqualityComparer);

            #endregion

            #region Overrides of InvalidatingPsiCache

            protected override void InvalidateOnPhysicalChange()
            {
                lock (_types)
                {
                    _types.Values.ForEach(provider => provider.Clear());
                    _types.Clear();
                }
            }

            #endregion

            #region Methods

            [NotNull]
            public TypeProvider GetOrCreateTypeProvider([NotNull] IPsiModule module,
                [NotNull] IModuleReferenceResolveContext context)
            {
                TypeProvider provider;
                lock (_types)
                {
                    Pair<IPsiModule, IModuleReferenceResolveContext> local1 = Pair.Of(module, context);
                    if (!_types.TryGetValue(local1, out provider))
                    {
                        provider = new TypeProvider(module, context);
                        _types[local1] = provider;
                    }
                }
                return provider;
            }

            #endregion
        }

        #endregion

        #region Fields

        public static readonly ClrTypeName ViewModelTypeRequiredAttributeClrName = new ClrTypeName(ClassNameConstants.ViewModelTypeRequiredAttribute);
        public static readonly ClrTypeName ShouldBeInitializedAttributeClrName = new ClrTypeName(ClassNameConstants.ShouldBeInitializedAttribute);
        public static readonly ClrTypeName NotEmptyParamsAttributeClrName = new ClrTypeName(ClassNameConstants.NotEmptyParamsAttribute);
        public static readonly ClrTypeName SuppressTaskBusyHandlerAttributeClrName = new ClrTypeName(ClassNameConstants.SuppressTaskBusyHandlerAttribute);
        public static readonly ClrTypeName WrapperAttributeClrName = new ClrTypeName(ClassNameConstants.WrapperAttribute);
        public static readonly ClrTypeName ViewModelAttributeClrName = new ClrTypeName(ClassNameConstants.ViewModelAttribute);
        public static readonly ClrTypeName PageViewModelAttributeClrName = new ClrTypeName(ClassNameConstants.PageViewModelAttribute);
        public static readonly ClrTypeName BaseViewModelAttributeClrName = new ClrTypeName(ClassNameConstants.BaseViewModelAttribute);
        public static readonly ClrTypeName BaseViewAttributeClrName = new ClrTypeName(ClassNameConstants.BaseViewAttribute);
        public static readonly ClrTypeName IgnorePropertyAttributeClrName = new ClrTypeName(ClassNameConstants.IgnorePropertyAttribute);

        private readonly SearchDomainFactory _searchDomainFactory;
        private readonly ConcurrentDictionary<string, IDeclaredType> _declaredTypes;
        private readonly IPsiModule _psiModule;
        private readonly IModuleReferenceResolveContext _resolveContext;
        private readonly ConcurrentDictionary<string, object> _objectCache;
        private readonly Lazy<bool> _isSupportedTask;

        private const string BaseViewModelTypesKey = "BaseViewModelTypesKey";
        private const string BaseViewTypesKey = "BaseViewTypesKey";
        private const string WrapperTypesKey = "WrapperTypes";
        private const string AllViewsKey = "AllViewsKey";
        private const string AllViewModelsKey = "AllViewModelsKey";
        private const string TaskBusyHandlersKey = "TaskBusyHandlersKey";

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="TypeProvider" /> class.
        /// </summary>
        private TypeProvider([NotNull] IPsiModule psiModule, [NotNull] IModuleReferenceResolveContext resolveContext)
        {
            if (psiModule == null) throw new ArgumentNullException("psiModule");
            if (resolveContext == null) throw new ArgumentNullException("resolveContext");
            _psiModule = psiModule;
            _resolveContext = resolveContext;
            _declaredTypes = new ConcurrentDictionary<string, IDeclaredType>();
            _objectCache = new ConcurrentDictionary<string, object>();
            _searchDomainFactory = SearchDomainFactory.Instance;
            _isSupportedTask = new Lazy<bool>(() =>
            {
                ITypeElement typeElement = CloseableViewModelInterface.GetTypeElement();
                if (typeElement == null)
                    return true;
                var type = (IDeclaredType)typeElement
                    .Methods
                    .Single(method => method.ShortName == MethodNameConstants.CloseableViewModelInterfaceClose)
                    .ReturnType;
                return type.GetClrName().ShortName == "Task";
            });
        }

        #endregion

        #region Properties

        [NotNull]
        public IDeclaredType Object
        {
            get { return GetDeclaredType(PredefinedType.OBJECT_FQN.FullName); }
        }

        [NotNull]
        public IDeclaredType String
        {
            get { return GetDeclaredType(PredefinedType.STRING_FQN.FullName); }
        }

        [NotNull]
        public IDeclaredType Task
        {
            get { return GetDeclaredType(PredefinedType.TASK_FQN.FullName); }
        }

        [NotNull]
        public IDeclaredType DisposableInterface
        {
            get { return GetDeclaredType(PredefinedType.IDISPOSABLE_FQN.FullName); }
        }

        [NotNull]
        public IDeclaredType CommandInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.Command); }
        }

        [NotNull]
        public IDeclaredType EnumerableInterface
        {
            get { return GetDeclaredType(PredefinedType.IENUMERABLE_FQN.FullName); }
        }

        [NotNull]
        public IDeclaredType FrameworkElement
        {
            get
            {
                IDeclaredType declaredType = GetDeclaredType(ClassNameConstants.FrameworkElement);
                if (declaredType.IsResolved)
                    return declaredType;
                return GetDeclaredType(ClassNameConstants.FrameworkElementWinRT);
            }
        }

        [NotNull]
        public IDeclaredType Page
        {
            get
            {
                IDeclaredType declaredType = GetDeclaredType(ClassNameConstants.UiPage);
                if (declaredType.IsResolved)
                    return declaredType;
                return GetDeclaredType(ClassNameConstants.UiPageWinRT);
            }
        }

        [NotNull]
        public IDeclaredType ViewModelAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.ViewModelAttribute); }
        }

        [NotNull]
        public IDeclaredType PageViewModelAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.PageViewModelAttribute); }
        }

        [NotNull]
        public IDeclaredType IgnorePropertyAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.IgnorePropertyAttribute); }
        }

        [NotNull]
        public IDeclaredType ModelPropertyAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.ModelPropertyAttribute); }
        }

        [NotNull]
        public IDeclaredType BaseViewModelAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.BaseViewModelAttribute); }
        }

        [NotNull]
        public IDeclaredType BaseViewAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.BaseViewAttribute); }
        }

        [NotNull]
        public IDeclaredType WrapperAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.WrapperAttribute); }
        }

        [NotNull]
        public IDeclaredType SuppressTaskBusyHandlerAttribute
        {
            get { return GetDeclaredType(ClassNameConstants.SuppressTaskBusyHandlerAttribute); }
        }

        [NotNull]
        public IDeclaredType Should
        {
            get { return GetDeclaredType(ClassNameConstants.Should); }
        }

        [NotNull]
        public IDeclaredType MvvmUtils
        {
            get { return GetDeclaredType(ClassNameConstants.MvvmUtils); }
        }

        [NotNull]
        public IDeclaredType ViewModelExtensions
        {
            get { return GetDeclaredType(ClassNameConstants.ViewModelExtensions); }
        }

        [NotNull]
        public IDeclaredType GlobalSettings
        {
            get { return GetDeclaredType(ClassNameConstants.GlobalSettings); }
        }

        [NotNull]
        public IDeclaredType CloseableViewModelFacade
        {
            get { return GetDeclaredType(ClassNameConstants.CloseableViewModelFacade); }
        }

        [NotNull]
        public IDeclaredType ViewModelInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.ViewModel); }
        }

        [NotNull]
        public IDeclaredType CloseableViewModelInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.CloseableViewModel); }
        }

        [NotNull]
        public IDeclaredType NotifyViewModelClosingInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.NotifyViewModelClosing); }
        }

        [NotNull]
        public IDeclaredType EditableViewModelInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.EditableViewModel); }
        }

        [NotNull]
        public IDeclaredType ViewInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.View); }
        }

        [NotNull]
        public IDeclaredType ViewInterfaceGeneric
        {
            get { return GetDeclaredType(InterfaceNameConstants.ViewGeneric); }
        }

        [NotNull]
        public IDeclaredType WindowViewInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.WindowView); }
        }

        [NotNull]
        public IDeclaredType ChildWindowViewInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.ChildWindowView); }
        }

        [NotNull]
        public IDeclaredType ViewModelBase
        {
            get { return GetDeclaredType(ClassNameConstants.ViewModelBase); }
        }

        [NotNull]
        public IDeclaredType ValidatableViewModel
        {
            get { return GetDeclaredType(ClassNameConstants.ValidatableViewModel); }
        }

        [NotNull]
        public IDeclaredType ValidatableViewModelInterface
        {
            get { return GetDeclaredType(InterfaceNameConstants.ValidatableViewModel); }
        }

        public IDeclaredType ViewModelClosingDelegate
        {
            get { return GetDeclaredType(ClassNameConstants.ViewModelClosingDelegate); }
        }

        public IDeclaredType ViewModelClosedDelegate
        {
            get { return GetDeclaredType(ClassNameConstants.ViewModelClosedDelegate); }
        }

        public bool IsSupportedTask
        {
            get { return _isSupportedTask.Value; }
        }

        public IPsiModule PsiModule
        {
            get { return _psiModule; }
        }

        public IModuleReferenceResolveContext ResolveContext
        {
            get { return _resolveContext; }
        }

        #endregion

        #region Methods

        [Pure]
        public static TypeProvider Create([NotNull] IPsiModule psiModule,
            [NotNull] IModuleReferenceResolveContext resolveContext)
        {
            return psiModule.GetPsiServices()
                .Caches
                .GetPsiCache<TypeProviderCache>()
                .GetOrCreateTypeProvider(psiModule, resolveContext);
        }

        [Pure]
        public static TypeProvider Create([NotNull] ICSharpContextActionDataProvider provider)
        {
            return Create(provider.PsiModule, provider.PsiFile.GetResolveContext());
        }

        [NotNull]
        public IDeclaredType GetDeclaredType(string typeName)
        {
            return _declaredTypes
                .GetOrAdd(typeName, s => new DeclaredTypeFromCLRName(new ClrTypeName(s), PsiModule, ResolveContext));
        }

        [NotNull]
        public IEnumerable<ViewModelMetaModel> GetBaseViewModelTypes()
        {
            return GetOrAdd(BaseViewModelTypesKey, () =>
            {
                if (!ViewModelInterface.IsResolved)
                    return Enumerable.Empty<ViewModelMetaModel>();
                if (BaseViewModelAttribute.IsResolved)
                    return GetBaseViewModelTypesUsingAttribute();
                var declaredTypes = new[]
                {
                    GetDeclaredType(ClassNameConstants.NotifyPropertyChangedBase),
                    GetDeclaredType(ClassNameConstants.ViewModelBase),
                    GetDeclaredType(ClassNameConstants.WorkspaceViewModel),
                    GetDeclaredType(ClassNameConstants.GridViewModel),
                    GetDeclaredType(ClassNameConstants.ValidatableViewModel),
                    GetDeclaredType(ClassNameConstants.EditableViewModel),
                    GetDeclaredType(ClassNameConstants.MultiViewModel),
                    GetDeclaredType(ClassNameConstants.NavigableViewModel),
                    GetDeclaredType(ClassNameConstants.WindowViewModel),
                    GetDeclaredType(ClassNameConstants.ChildWindowViewModel)
                };
                return declaredTypes
                    .Select((type, i) => new ViewModelMetaModel(type, type.GetClrName().ShortName, declaredTypes.Length - i))
                    .ToArray();
            });
        }

        [NotNull]
        public IEnumerable<ViewMetaModel> GetBaseViewTypes()
        {
            return GetOrAdd(BaseViewTypesKey, () =>
            {
                if (!ViewInterface.IsResolved)
                    return Enumerable.Empty<ViewMetaModel>();
                if (BaseViewModelAttribute.IsResolved)
                    return GetBaseViewTypesUsingAttribute();
                return new[]
                {
                    new ViewMetaModel(GetDeclaredType(InterfaceNameConstants.View), null, "IView", 0),
                    new ViewMetaModel(GetDeclaredType(InterfaceNameConstants.ChildWindowView),
                        GetDeclaredType(ClassNameConstants.ChildWindow), "IChildWindowView", 0),
                    new ViewMetaModel(GetDeclaredType(InterfaceNameConstants.WindowView),
                        GetDeclaredType(ClassNameConstants.Window), "IWindowView", 0)
                };
            });
        }

        [NotNull]
        public IEnumerable<WrapperMetaModel> GetWrapperTypes()
        {
            return GetOrAdd(WrapperTypesKey,
                () =>
                    FindInheritors<ITypeElement, WrapperMetaModel>(ViewModelInterface, true, false, true,
                        (element, collector) =>
                        {
                            IAttributeInstance attribute = element
                                .GetAttributeInstances(WrapperAttributeClrName, false)
                                .FirstOrDefault();
                            if (attribute != null)
                                collector.Add(new WrapperMetaModel(element, attribute, this));
                        }));
        }

        [NotNull]
        public IEnumerable<TaskBusyHandlerMetaModel> GetTaskBusyHandlers()
        {
            return GetOrAdd(TaskBusyHandlersKey, () =>
            {
                var declaredType = GetDeclaredType(ClassNameConstants.MvvmExtensions);
                var @class = declaredType.GetTypeElement() as IClass;
                if (@class == null)
                    return Enumerable.Empty<TaskBusyHandlerMetaModel>();
                var methods = @class.Methods
                    .Where(method => method.ShortName == MethodNameConstants.MvvmExtensionsWithBusyIndicator)
                    .Where(method => method.Parameters.Count == 4 && Equals(method.Parameters[2].Type, Object));
                return methods
                    .Select(method => new TaskBusyHandlerMetaModel(@class, method, method.Parameters[1], method.Parameters[2], true))
                    .ToArray();
            });
        }

        [NotNull]
        public IEnumerable<IClass> FindAllViews()
        {
            return GetOrAdd(AllViewsKey, () => FindInheritors<IClass>(ViewInterface, true, true, true, null));
        }

        [NotNull]
        public IEnumerable<IClass> FindAllViewModels()
        {
            return GetOrAdd(AllViewModelsKey, () => FindInheritors<IClass>(ViewModelInterface, true, false, true, null));
        }

        [NotNull]
        public IEnumerable<IClass> FindViewsForViewModel([NotNull] IClass viewModelClass)
        {
            if (viewModelClass == null)
                throw new ArgumentNullException("viewModelClass");
            IDeclaredType viewModelType = TypeFactory.CreateType(viewModelClass);
            return FindAllViews()
                .Where(view => FindViewModelsForView(view).Any(type => type.IsOfType(viewModelType)))
                .ToArray();
        }

        [NotNull]
        public IList<IDeclaredType> FindViewModelsForView([NotNull] IClass viewClass)
        {
            if (viewClass == null)
                throw new ArgumentNullException("viewClass");
            var vmAttributes = viewClass.GetAttributeInstances(ViewModelAttributeClrName, true);
            var pvmAttributes = viewClass.GetAttributeInstances(PageViewModelAttributeClrName, true);

            var list = new List<IDeclaredType>();
            foreach (IAttributeInstance attributeInstance in vmAttributes.Concat(pvmAttributes).Where(instance => instance.PositionParameterCount != 0))
            {
                AttributeValue vmType = attributeInstance.PositionParameter(0);
                var declaredType = vmType.TypeValue as IDeclaredType;
                if (declaredType != null && declaredType.IsOfType(ViewModelInterface))
                    list.Add(declaredType);
            }
            return list;
        }

        [NotNull]
        private IEnumerable<ViewModelMetaModel> GetBaseViewModelTypesUsingAttribute()
        {
            return FindInheritors<IClass, ViewModelMetaModel>(ViewModelInterface, true, false, true,
                (result, collector) =>
                {
                    var attribute = result
                        .GetAttributeInstances(BaseViewModelAttributeClrName, false)
                        .FirstOrDefault();
                    if (attribute != null)
                        collector.Add(new ViewModelMetaModel(result, attribute));
                },
                models => models.Add(new ViewModelMetaModel(GetDeclaredType(ClassNameConstants.NotifyPropertyChangedBase),
                        "NotifyPropertyChangedBase", 10)));
        }

        [NotNull]
        private IEnumerable<ViewMetaModel> GetBaseViewTypesUsingAttribute()
        {
            return FindInheritors<IInterface, ViewMetaModel>(ViewInterface, true, false, true, (element, collector) =>
            {
                var attribute = element
                    .GetAttributeInstances(BaseViewAttributeClrName, false)
                    .FirstOrDefault();
                if (attribute != null)
                    collector.Add(new ViewMetaModel(element, attribute, this));
            }, models => models.Add(new ViewMetaModel(ViewInterface, null, "IView", 1)));
        }

        private void Clear()
        {
            _declaredTypes.Clear();
            _objectCache.Clear();
        }

        private T GetOrAdd<T>(string key, Func<T> getValue)
        {
            return (T)_objectCache.GetOrAdd(key, s => getValue());
        }

        [NotNull]
        private IEnumerable<TResult> FindInheritors<T, TResult>([NotNull] IDeclaredType type, bool withReferences, bool includeRelatedModules, bool requiredToolkitReference,
            [NotNull] ConverterDelegate<T, TResult> converter, Action<ICollection<TResult>> updateItemsAction = null)
            where T : class, ITypeElement
        {
            if (type == null)
                throw new ArgumentNullException("type");
            if (converter == null)
                throw new ArgumentNullException("converter");
            var typeElement = type.GetTypeElement();
            if (!type.IsResolved || typeElement == null)
                return Enumerable.Empty<TResult>();

            var psiModules = new List<IPsiModule> { PsiModule };
            if (includeRelatedModules)
                psiModules.AddRange(PsiModule.GetRelatedModules(ResolveContext));
            if (withReferences)
                psiModules.AddRange(PsiModule.GetReferences(ResolveContext, requiredToolkitReference));

            var psiServices = PsiModule.GetPsiServices();
            var results = new HashSet<TResult>();
            var searchDomain = _searchDomainFactory.CreateSearchDomain(psiModules);
            var findResultConsumer = new FindResultConsumer(result =>
            {
                var declaredElement = result as FindResultDeclaredElement;
                if (declaredElement != null)
                {
                    var element = declaredElement.DeclaredElement as T;
                    if (element != null)
                        converter(element, results);
                }
                return FindExecution.Continue;
            });
            psiServices.Finder.FindInheritors(typeElement, searchDomain, findResultConsumer, NullProgressIndicator.Instance);
            if (updateItemsAction != null)
                updateItemsAction(results);
            return results.ToArray();
        }

        [NotNull]
        private IEnumerable<T> FindInheritors<T>([NotNull] IDeclaredType type, bool withReferences, bool includeRelatedModules, bool requiredToolkitReference, Func<T, bool> filter, Action<ICollection<T>> updateItemsAction = null)
            where T : class, ITypeElement
        {
            if (type == null)
                throw new ArgumentNullException("type");
            ConverterDelegate<T, T> converter = (element, collector) =>
            {
                if (filter == null || filter(element))
                    collector.Add(element);
            };
            return FindInheritors(type, withReferences, includeRelatedModules, requiredToolkitReference, converter,
                updateItemsAction);
        }

        #endregion
    }
}